package service

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/pkg/errors"
	"gorm.io/gorm"
	"inventory/api/inventory"
	"inventory/internal/constant"
	"math"
	"sort"

	"github.com/golang/geo/s2"

	"inventory/internal/model"
	"inventory/pkg/ecode"
)

func rankMapIntFloat(values map[uint32]float64) []uint32 {
	type kv struct {
		Key   uint32
		Value float64
	}
	var ss []kv
	for k, v := range values {
		ss = append(ss, kv{k, v})
	}
	sort.Slice(ss, func(i, j int) bool {
		return ss[i].Value < ss[j].Value
	})
	ranked := make([]uint32, len(values))
	for i, kv := range ss {
		ranked[i] = kv.Key
	}
	return ranked
}

func getNearestWarehouse(allDistanceMap map[uint32]float64, warehouseMap model.WarehouseMap,
	targetLocation s2.LatLng) *model.Warehouse {
	if len(warehouseMap) == 0 {
		return nil
	}
	distanceMap := make(map[uint32]float64)
	for warehouseID, warehouse := range warehouseMap {
		if _, ok := allDistanceMap[warehouseID]; ok {
			distanceMap[warehouseID] = allDistanceMap[warehouseID]
		} else {
			ll := s2.LatLngFromDegrees(warehouse.Lat, warehouse.Lng)
			distanceMap[warehouseID] = math.Round(ll.Distance(targetLocation).Degrees()*100) / 100
			allDistanceMap[warehouseID] = distanceMap[warehouseID]
		}
	}
	rankMap := rankMapIntFloat(distanceMap)
	// 不符合和总部优先规则时，返回最近的仓库
	return warehouseMap[rankMap[0]]
}

func (s *Service) matchStore(ctx context.Context, req *inventory.MatchWarehouseRequest) (resp *inventory.MatchWarehouseResponse, err error) {
	// 指定门店刷新仓库信息流程，独立处理
	var (
		warehouse *model.Warehouse
		stores    []*inventory.StoreView
	)
	if warehouse, err = s.dao.GetWarehouseInfoByStoreID(ctx, req.StoreId); err != nil {
		if errors.Cause(err) == gorm.ErrRecordNotFound {
			err = constant.ErrWarehouseNotExist
			return
		}
		return
	}
	if err = s.dao.CheckWarehouseStatus(warehouse); err != nil {
		return
	}
	if warehouse.SgID != req.CustomerToken.SgId {
		err = constant.ErrWarehouseNotExist
		return
	}
	if stores, err = s.getWarehousesExpressViews(
		ctx, []*model.Warehouse{warehouse}, req.Lat, req.Lng, req.CustomerToken.SgId, req.CityCode); err != nil {
		return
	}
	store := stores[0]
	if len(store.CityRegions) == 0 && store.OrdinaryExpressConfig == nil && (warehouse.CityCode != req.CityCode || !warehouse.PickUp) {
		// 同城&普快&自提均不支持时
		err = constant.ErrNoMatchWarehouse
		return
	}
	resp = &inventory.MatchWarehouseResponse{
		Id:                    store.WarehouseId,
		StoreId:               store.Id,
		Name:                  store.Name,
		Address:               store.Address,
		Distance:              store.Distance,
		PickUp:                store.PickUp,
		CityExpress:           store.CityExpress,
		OrdinaryExpress:       store.OrdinaryExpress,
		DeadLineTime:          store.DeadLineTime,
		CityRegions:           store.CityRegions,
		OrdinaryExpressConfig: store.OrdinaryExpressConfig,
	}
	return
}

// MatchWarehouse 匹配仓库
func (s *Service) MatchWarehouse(ctx context.Context, req *inventory.MatchWarehouseRequest) (resp *inventory.MatchWarehouseResponse, err error) {
	if req.StoreId != 0 {
		if resp, err = s.matchStore(ctx, req); err != nil {
			return
		}
		return
	}
	var (
		warehouses     []*model.Warehouse
		expressRegions []*model.ExpressCoordinates
		expressCities  []*model.ExpressCity
		matchWarehouse *model.Warehouse
	)
	if warehouses, err = s.dao.FindAllHQWarehousesBySgID(ctx, req.CustomerToken.SgId); err != nil {
		return
	}
	fmt.Printf("-----req.CustomerToken.SgId:%d", req.CustomerToken.SgId)
	if len(warehouses) == 0 {
		err = constant.ErrNoMatchWarehouse
		return
	}
	warehouseMap := make(model.WarehouseMap)
	deliverMap := make(model.WarehouseMap)
	matchCityMap := make(map[uint32]*inventory.OrdinaryExpressConfig)
	matchRegionMap := make(map[uint32][]*inventory.ExpressRegionView)
	for _, warehouse := range warehouses {
		warehouseMap[warehouse.ID] = warehouse
		switch req.DeliverType {
		case inventory.DeliverType_PICK_UP:
			if warehouse.PickUp {
				deliverMap[warehouse.ID] = warehouse
			}
		default:
			deliverMap[warehouse.ID] = warehouse
		}
	}
	if len(deliverMap) == 0 {
		err = constant.ErrNoMatchWarehouse
		return
	}
	targetLocation := s2.LatLngFromDegrees(req.Lat, req.Lng)
	targetPoint := s2.PointFromLatLng(targetLocation)
	allDistanceMap := make(map[uint32]float64)
	switch req.DeliverType {
	case inventory.DeliverType_ORDINARY:
		if expressCities, err = s.dao.FindExpressCitiesByCityCodes(
			ctx, req.CustomerToken.SgId, []uint32{
				req.CityCode - req.CityCode%100}, deliverMap.GetKeys()); err != nil {
			return
		}
		for _, ec := range expressCities {
			matchCityMap[ec.WarehouseID] = &inventory.OrdinaryExpressConfig{
				First:      ec.FirstWeightPrice,
				Additional: ec.AdditionalWeightPrice,
			}
		}
		for _, warehouseID := range deliverMap.GetKeys() {
			if _, ok := matchCityMap[warehouseID]; !ok {
				delete(deliverMap, warehouseID)
			}
		}
	case inventory.DeliverType_CITY:
		var matchRegions map[uint32][]*model.ExpressCoordinates
		if expressRegions, err = s.dao.FindExpressRegionsByWarehouseIDs(
			ctx, req.CustomerToken.SgId, req.CityCode, deliverMap.GetKeys()); err != nil {
			return
		}
		if matchRegions, err = MatchRegionsByLatLng(expressRegions, targetPoint); err != nil {
			return
		}
		for _, warehouseId := range deliverMap.GetKeys() {
			if regions, ok := matchRegions[warehouseId]; ok {
				matchRegionMap[warehouseId] = []*inventory.ExpressRegionView{}
				for _, region := range regions {
					matchRegionMap[warehouseId] = append(matchRegionMap[warehouseId], &inventory.ExpressRegionView{
						StartFee:   region.StartingFee,
						ExpressFee: region.ShippingFee,
					})
				}
			} else {
				delete(deliverMap, warehouseId)
			}
		}
	}
	if len(deliverMap) == 0 {
		err = constant.ErrNoMatchWarehouse
		return
	}
	matchWarehouse = getNearestWarehouse(allDistanceMap, deliverMap, targetLocation)
	resp = &inventory.MatchWarehouseResponse{
		Id:           matchWarehouse.ID,
		StoreId:      matchWarehouse.StoreID,
		Name:         matchWarehouse.Name,
		Address:      matchWarehouse.Address,
		Distance:     allDistanceMap[matchWarehouse.ID],
		PickUp:       matchWarehouse.PickUp && matchWarehouse.CityCode == req.CityCode,
		DeadLineTime: matchWarehouse.Deadline.String(),
		CityRegions:  []*inventory.ExpressRegionView{},
	}
	// 填充同城配送范围
	if regions, ok := matchRegionMap[matchWarehouse.ID]; ok {
		resp.CityRegions = regions
		resp.CityExpress = true
	}
	// 填充普通配置数据
	if city, ok := matchCityMap[matchWarehouse.ID]; ok {
		resp.OrdinaryExpressConfig = city
		resp.OrdinaryExpress = true
	}
	return
}

func (s *Service) getWarehousesExpressViews(ctx context.Context, warehouses []*model.Warehouse,
	lat, lng float64, sgID, cityCode uint32) (stores []*inventory.StoreView, err error) {
	var (
		warehouseIDs, cityWarehouseIDs, expressWarehouseIDs []uint32
		expressRegions                                      []*model.ExpressCoordinates
		expressCities                                       []*model.ExpressCity
	)
	targetLocation := s2.LatLngFromDegrees(lat, lng)
	targetPoint := s2.PointFromLatLng(targetLocation)
	distanceMap := make(map[uint32]float64)
	cityRegionMap := make(map[uint32][]*inventory.ExpressRegionView)
	ordinaryMap := make(map[uint32]*inventory.OrdinaryExpressConfig)
	warehouseMap := make(map[uint32]*model.Warehouse)
	for _, w := range warehouses {
		warehouseIDs = append(warehouseIDs, w.ID)
		warehouseMap[w.ID] = w
		if w.CityExpress {
			// 同城仓库列表
			cityWarehouseIDs = append(cityWarehouseIDs, w.ID)
		}
		if w.OrdinaryExpress {
			expressWarehouseIDs = append(expressWarehouseIDs, w.ID)
		}
		if _, ok := distanceMap[w.ID]; !ok {
			ll := s2.LatLngFromDegrees(w.Lat, w.Lng)
			distanceMap[w.ID] = math.Round(ll.Distance(targetLocation).Degrees()*100) / 100
		}
	}
	if len(cityWarehouseIDs) > 0 {
		var matchRegions map[uint32][]*model.ExpressCoordinates
		if expressRegions, err = s.dao.FindExpressRegionsByWarehouseIDs(
			ctx, sgID, cityCode, warehouseIDs); err != nil {
			return
		}
		if matchRegions, err = MatchRegionsByLatLng(expressRegions, targetPoint); err != nil {
			return
		}
		for warehouseId, regions := range matchRegions {
			cityRegionMap[warehouseId] = []*inventory.ExpressRegionView{}
			for _, region := range regions {
				cityRegionMap[warehouseId] = append(cityRegionMap[warehouseId], &inventory.ExpressRegionView{
					StartFee:   region.StartingFee,
					ExpressFee: region.ShippingFee,
				})
			}
		}
	}
	if len(expressWarehouseIDs) > 0 {
		if expressCities, err = s.dao.FindExpressCitiesByCityCodes(
			ctx, sgID, []uint32{cityCode - cityCode%100}, expressWarehouseIDs); err != nil {
			return
		}
		for _, ec := range expressCities {
			ordinaryMap[ec.WarehouseID] = &inventory.OrdinaryExpressConfig{
				First:      ec.FirstWeightPrice,
				Additional: ec.AdditionalWeightPrice,
			}
		}
	}
	stores = make([]*inventory.StoreView, len(warehouses))
	for i, w := range warehouses {
		stores[i] = &inventory.StoreView{
			Id:           w.StoreID,
			WarehouseId:  w.ID,
			Name:         w.Name,
			Address:      w.Address,
			Distance:     distanceMap[w.ID],
			PickUp:       w.PickUp && cityCode == w.CityCode,
			DeadLineTime: w.Deadline.String(),
			CityRegions:  []*inventory.ExpressRegionView{},
		}
		if regions, ok := cityRegionMap[w.ID]; ok {
			stores[i].CityExpress = true
			stores[i].CityRegions = regions
		}
		if matchCity, ok := ordinaryMap[w.ID]; ok {
			stores[i].OrdinaryExpress = true
			stores[i].OrdinaryExpressConfig = matchCity
		}
	}
	return
}

// GetWarehouseStores 获取附近的门店仓列表
func (s *Service) GetWarehouseStores(ctx context.Context, req *inventory.GetWarehouseStoresRequest) (
	resp *inventory.GetWarehouseStoresResponse, err error) {
	var (
		warehouses []*model.Warehouse
		stores     []*inventory.StoreView
		matchRes   *inventory.MatchWarehouseResponse
	)
	if warehouses, err = s.dao.FindWarehouseStores(ctx, req.CustomerToken.SgId); err != nil {
		return
	}
	if stores, err = s.getWarehousesExpressViews(
		ctx, warehouses, req.Lat, req.Lng, req.CustomerToken.SgId, req.CityCode); err != nil {
		return
	}
	if req.HqMatch {
		// 本接口仅门店专场调用，在首页插入自营仓
		if matchRes, err = s.MatchWarehouse(ctx, &inventory.MatchWarehouseRequest{
			CustomerToken: req.CustomerToken,
			Lat:           req.Lat,
			Lng:           req.Lng,
			CityCode:      req.CityCode,
		}); err == nil {
			tmp := append([]*inventory.StoreView{}, &inventory.StoreView{
				Id:                    0,
				WarehouseId:           matchRes.Id,
				Name:                  "品牌自营",
				Address:               matchRes.Address,
				Distance:              matchRes.Distance,
				PickUp:                matchRes.PickUp,
				CityExpress:           matchRes.CityExpress,
				OrdinaryExpress:       matchRes.OrdinaryExpress,
				DeadLineTime:          matchRes.DeadLineTime,
				CityRegions:           matchRes.CityRegions,
				OrdinaryExpressConfig: matchRes.OrdinaryExpressConfig,
			})
			tmp = append(tmp, stores[0:]...)
			stores = tmp
		} else {
			// 无匹配时，不报错，也不返回总部仓
			err = nil
		}
	}
	resp = &inventory.GetWarehouseStoresResponse{
		Stores: stores,
	}
	return
}

// MatchRegionsByLatLng 筛选所有包含查询坐标点的区域
func MatchRegionsByLatLng(expressRegions []*model.ExpressCoordinates, targetPoint s2.Point) (
	matched map[uint32][]*model.ExpressCoordinates, err error) {
	// 筛选所有包含查询坐标点的区域
	matched = make(map[uint32][]*model.ExpressCoordinates)
	for _, r := range expressRegions {
		polygons := make([][][]float64, len(expressRegions))
		if err = json.Unmarshal(r.Coordinates, &polygons); err != nil {
			return
		}
		// 匹配每个多边形区域
		var pts []s2.Point
		for _, pp := range polygons {
			for _, p := range pp {
				pts = append(pts, s2.PointFromLatLng(s2.LatLngFromDegrees(p[1], p[0])))
			}
			loop := s2.LoopFromPoints(pts)
			polygon := s2.PolygonFromLoops([]*s2.Loop{loop})
			if polygon.ContainsPoint(targetPoint) {
				matched[r.WarehouseID] = append(matched[r.WarehouseID], r)
			}
		}
	}
	return
}

// GetProds 查看用户指定仓库多个商品库存情况
func (s *Service) GetProds(ctx context.Context, req *inventory.GetProdsRequest) (resp *inventory.GetProdsResponse, err error) {
	var (
		warehouseID uint32
		prodQuery   []*inventory.MultipleProductQuery
		prodResp    *inventory.GetMultipleProdSkusReply
	)
	prods := make([]*inventory.GetProdsView, 0)
	if req.StoreId > 0 {
		var warehouse *model.Warehouse
		if warehouse, err = s.dao.GetWarehouseInfoByStoreID(ctx, req.StoreId); err != nil {
			if errors.Cause(err) == gorm.ErrRecordNotFound {
				err = constant.ErrWarehouseNotExist
			}
			return
		}
		if err = s.dao.CheckWarehouseStatus(warehouse); err != nil {
			return
		}
		warehouseID = warehouse.ID
	} else {
		warehouseID = req.WarehouseId
		if warehouseID == 0 {
			err = ecode.BadRequest("门店ID、仓库ID必须传一个")
			return
		}
	}
	for _, prod := range req.Prods {
		prodQuery = append(prodQuery, &inventory.MultipleProductQuery{
			Id:     warehouseID,
			ProdId: prod.Id,
			SkuId:  prod.SkuId,
		})
	}
	if prodResp, err = s.GetMultipleProdSkus(ctx, &inventory.GetMultipleProdSkusRequest{
		Prods: prodQuery, WebToken: &inventory.WebToken{
			EmpId: 0,
			SgId:  req.CustomerToken.SgId,
		}}); err != nil {
		return
	}
	for _, prod := range prodResp.Prods {
		quantity := -1
		if prod.Storage != nil {
			quantity = int(prod.Storage.Value)
		}
		prods = append(prods, &inventory.GetProdsView{
			Id:      prod.ProdId,
			SkuId:   prod.SkuId,
			Storage: uint64(quantity),
		})
	}
	resp = &inventory.GetProdsResponse{Prods: prods}
	return
}

// MatchSku 基于指定配送方式查看指定规格总部商品最近有存货的仓库信息和库存信息
func (s *Service) MatchSku(ctx context.Context, req *inventory.MatchSkuRequest) (resp *inventory.MatchSkuResponse, err error) {
	var (
		warehouses                                  []*model.Warehouse
		storages                                    []*model.Storage
		warehouseIDs, ordinaryIDs, cityIDs, pickIDs []uint32
		storageQuantity                             float64
	)
	sk := fmt.Sprintf("%d-%d", req.ProdId, req.SkuId)
	if storages, err = s.dao.FindStoragesByStorageKeys(ctx, []string{sk}); err != nil {
		return
	}
	for _, storage := range storages {
		if storage.Quantity-storage.FreezeQuantity > 0 {
			warehouseIDs = append(warehouseIDs, storage.WarehouseID)
			storageQuantity += storage.Quantity - storage.FreezeQuantity
		}
	}
	warehouseMap := make(map[uint32]*model.Warehouse)
	if warehouses, err = s.dao.FindWarehousesByIDsAndDeliverType(ctx, warehouseIDs, req.DeliveryType); err != nil {
		return
	}
	for _, w := range warehouses {
		warehouseMap[w.ID] = w
		if w.CityCode == req.CityCode && w.PickUp {
			pickIDs = append(pickIDs, w.ID)
		}
		if w.CityExpress {
			cityIDs = append(cityIDs, w.ID)
		}
		if w.OrdinaryExpress {
			ordinaryIDs = append(ordinaryIDs, w.ID)
		}
	}
	matchRegionMap := make(map[uint32][]*inventory.ExpressRegionView)
	distanceMap := make(map[uint32]float64)
	matchCityMap := make(map[uint32]*inventory.OrdinaryExpressConfig)
	targetLocation := s2.LatLngFromDegrees(req.Lat, req.Lng)
	targetPoint := s2.PointFromLatLng(targetLocation)
	switch req.DeliveryType {
	case inventory.DeliverType_CITY:
		var (
			expressRegions []*model.ExpressCoordinates
			matchRegions   map[uint32][]*model.ExpressCoordinates
		)
		if len(cityIDs) > 0 {
			if expressRegions, err = s.dao.FindExpressRegionsByWarehouseIDs(
				ctx, req.CustomerToken.SgId, req.CityCode, cityIDs); err != nil {
				return
			}
			if matchRegions, err = MatchRegionsByLatLng(expressRegions, targetPoint); err != nil {
				return
			}
			for k, regions := range matchRegions {
				if _, ok := distanceMap[k]; !ok {
					ll := s2.LatLngFromDegrees(warehouseMap[k].Lat, warehouseMap[k].Lng)
					distanceMap[k] = math.Round(ll.Distance(targetLocation).Degrees()*100) / 100
				}
				matchRegionMap[k] = []*inventory.ExpressRegionView{}
				for _, region := range regions {
					matchRegionMap[k] = append(matchRegionMap[k], &inventory.ExpressRegionView{
						StartFee:   region.StartingFee,
						ExpressFee: region.ShippingFee,
					})
				}
			}
		}
	case inventory.DeliverType_ORDINARY:
		var expressCities []*model.ExpressCity
		if len(ordinaryIDs) > 0 {
			if expressCities, err = s.dao.FindExpressCitiesByCityCodes(
				ctx, req.CustomerToken.SgId, []uint32{req.CityCode - req.CityCode%100},
				ordinaryIDs); err != nil {
				return
			}
		}
		for _, ec := range expressCities {
			if _, ok := distanceMap[ec.WarehouseID]; !ok {
				ll := s2.LatLngFromDegrees(warehouseMap[ec.WarehouseID].Lat, warehouseMap[ec.WarehouseID].Lng)
				distanceMap[ec.WarehouseID] = math.Round(ll.Distance(targetLocation).Degrees()*100) / 100
			}
			matchCityMap[ec.WarehouseID] = &inventory.OrdinaryExpressConfig{
				First:      ec.FirstWeightPrice,
				Additional: ec.AdditionalWeightPrice,
			}
		}
	case inventory.DeliverType_PICK_UP:
		for _, warehouseId := range pickIDs {
			if _, ok := distanceMap[warehouseId]; !ok {
				ll := s2.LatLngFromDegrees(warehouseMap[warehouseId].Lat, warehouseMap[warehouseId].Lng)
				distanceMap[warehouseId] = math.Round(ll.Distance(targetLocation).Degrees()*100) / 100
			}
		}
	}
	if len(distanceMap) == 0 {
		err = ecode.BadRequest("匹配不到任何符合条件的仓库")
		return
	}
	// 获取距离最近的仓库
	rank := rankMapIntFloat(distanceMap)
	nearestWarehouse := warehouseMap[rank[0]]
	resp = &inventory.MatchSkuResponse{
		Id:           nearestWarehouse.ID,
		StoreId:      nearestWarehouse.StoreID,
		Name:         nearestWarehouse.Name,
		Address:      nearestWarehouse.Address,
		Storage:      uint64(storageQuantity),
		Distance:     distanceMap[nearestWarehouse.ID],
		DeadlineTime: "",
		CityRegions:  []*inventory.ExpressRegionView{},
	}
	// 填充同城配送范围
	if regions, ok := matchRegionMap[nearestWarehouse.ID]; ok {
		resp.CityRegions = regions
	}
	// 填充普通配置数据
	if ordinaryConfig, ok := matchCityMap[nearestWarehouse.ID]; ok {
		resp.OrdinaryExpressConfig = ordinaryConfig
	}
	return
}
